<?php
/**
 * Nuwani PHP IRC Bot Framework
 * Copyright (c) 2006-2010 The Nuwani Project
 *
 * Nuwani is a framework for IRC Bots built using PHP. Nuwani speeds up bot 
 * development by handling basic tasks as connection- and bot management, timers
 * and module managing. Features for your bot can easily be added by creating
 * your own modules, which will receive callbacks from the framework.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @copyright Copyright (c) 2006-2010 The Nuwani Project
 * @package Nuwani
 * @author Peter Beverloo <peter@lvp-media.com>
 * @see http://nuwani.googlecode.com
 */

require_once __DIR__ . '/PHPUnit/Framework.php';

/**
 * Running all automated tests available for the Nuwani system is something this
 * class controls. We'll run everything and ensure the results can be retrieved
 * easily so they can be handled in whatever way required.
 *
 * @package Testing
 */
class TestRunner
{
        /**
         * All test-files which have to be checked will be included in this property.
         * There are several methods available (addFile, addDirectory) which are
         * capable of scanning everything and adding the found files.
         *
         * @var array
         */
        
        private $testList;
        
        /**
         * There are three things which need to be done in order to initialize this
         * class. Firstly, PHPUnit has to be loaded (for which we need to adjust the
         * include path), after which an array may be initialized for the tests.
         */
        
        public function __construct ()
        {
                $this -> testList = array ();
        }
        
        /**
         * Testing a single file certainly is a possibility as well, which can be
         * done by supplying it to this method. We won't check whether the file
         * contains any tests, as that'll be left to do for PHPUnit.
         *
         * @param string $fileName Path to a file containing the tests.
         * @return TestRunner Active instance to allow call-chaining.
         */
        
        public function addFile ($fileName)
        {
                if (file_exists ($fileName))
                {
                        $this -> testList [] = $fileName;
                }
                
                return $this;
        }
        
        /**
         * Adding a directory to the testing system should be done by calling
         * this method with the directory you'd like to add. The scanning happens
         * recursively, so all child-directories will be added as well.
         *
         * @param string $directoryName Path of the directory which should be added.
         * @return TestRunner Active instance to allow call-chaining.
         */
        
        public function addDirectory ($directoryName)
        {
                if (file_exists ($directoryName) && is_dir ($directoryName))
                {
                        $directoryIterator = new \ RecursiveDirectoryIterator (__DIR__);
                        $iteratorIterator  = new \ RecursiveIteratorITerator ($directoryIterator);
                        $actualTestFiles   = new \ RegexIterator ($iteratorIterator, '/^.+\.test\.php$/i', \ RecursiveRegexIterator :: GET_MATCH);
                
                        foreach ($actualTestFiles as $fileNameArray)
                        {
                                if (!is_array ($fileNameArray) || count ($fileNameArray) != 1)
                                {
                                        /** the data-structure is wrong, don't add it **/
                                        continue ;
                                }
                                
                                $fileName = $fileNameArray [0];
                                if (is_string ($fileName) && file_exists ($fileName))
                                {
                                        $this -> testList [] = $fileName;
                                }
                        }
                }
                
                return $this;
        }
        
        /**
         * Running all the tests will be done through this method. Mind that,
         * while it does return an array, it's completely synchronous and locks
         * the process for a significant amount of time.
         *
         * The returned array will have these indexes:
         *   [runtime] = number of seconds it took to run the test-case.
         *   [count]   = number of tests which have been ran.
         *
         *   [success] = An array with all the tests which succeeded.
         *   [skipped] = An array with all the skipped tests.
         *   [failed]  = An array with all the failed tests.
         *
         * @return array An array with the results from this test.
         */
        
        public function run ()
        {
                $suite     = new \ PHPUnit_Framework_TestSuite  ('Nuwani');
                $results   = new \ PHPUnit_Framework_TestResult ();
                $startTime = time ();
                
                $resultListener = new TestRunnerResultListener;
                
                foreach ($this -> testList as $fileName)
                {
                        $suite -> addTestFile ($fileName);
                }
                
                
                $results -> addListener ($resultListener);
                $suite   -> run ($results);
                
                return array
                (
                        'runtime'       => time () - $startTime,
                        'count'         => $resultListener -> tests,
                        
                        'success'       => $resultListener -> success,
                        'skipped'       => $resultListener -> skipped,
                        'failed'        => $resultListener -> failed
                );
        }

};

/**
 * We need to implement a basic listening shell allowing us listen to any
 * test-results. Documentation is lacking, as it really doesn't matter.
 *
 * @package Testing
 */
class TestRunnerResultListener implements \ PHPUnit_Framework_TestListener
{
        public  $success = array ();
        public  $skipped = array ();
        public  $failed  = array ();
        
        private $active = null;
        public  $tests = 0;
        
        public function addError (\ PHPUnit_Framework_Test $test, \ Exception $e, $time)
        {
                $this -> active = null;
                $this -> failed [] = array
                (
                        'test'          => $test,
                        'reason'        => $e -> getMessage ()
                );
        }
        
        public function addFailure (\ PHPUnit_Framework_Test $test, \ PHPUnit_Framework_AssertionFailedError $e, $time)
        {
                $this -> active = null;
                $this -> failed [] = array
                (
                        'test'          => $test,
                        'reason'        => $e -> getMessage ()
                );
        }
        
        public function addIncompleteTest (\ PHPUnit_Framework_Test $test, \ Exception $e, $time)
        {
                $this -> active = null;
                $this -> failed [] = array
                (
                        'test'          => $test,
                        'reason'        => $e -> getMessage ()
                );
        }
        
        public function addSkippedTest (\ PHPUnit_Framework_Test $test, \ Exception $e, $time)
        {
                $this -> active = null;
                $this -> skipped [] = $test -> getName ();
        }
        
        public function startTest (\ PHPUnit_Framework_Test $test)
        {
                $this -> active = $test;
                $this -> tests ++;
        }
        
        public function endTest (\ PHPUnit_Framework_Test $test, $time)
        {
                if ($this -> active != null)
                {
                        $this -> active = null;
                        $this -> success [] = $test -> getName ();
                }
        }
        
        public function startTestSuite (\ PHPUnit_Framework_TestSuite $suite) {}
        public function endTestSuite (\ PHPUnit_Framework_TestSuite $suite) {}
};

/**
 * Global-scope code, oh no. It actually isn't so bad -- while I am certainly
 * aware that there are cleaner solutions, it's rather convenient to be able
 * and have the test-suite available from the CLI.
 */

if (isset ($_SERVER ['SCRIPT_NAME']) && basename ($_SERVER ['SCRIPT_NAME']) == basename (__FILE__))
{
        $runner  = new TestRunner ();
        $runner -> addDirectory (__DIR__);
        
        echo 'Nuwani Test Runner (v1.0.' . trim (substr ('$Revision: 151 $', 10, -1)) . ')' . PHP_EOL;
        echo '---------------------------------------------------------------------------------' . PHP_EOL . PHP_EOL;
        
        $testResults = $runner -> run ();
        
        echo '    Ran ' . $testResults ['count'] . ' tests.' . PHP_EOL . PHP_EOL;
        
        echo '    SUCCESS: ' . count ($testResults ['success']) . ' tests' . PHP_EOL;
        echo '    SKIPPED: ' . count ($testResults ['skipped']) . ' tests' . PHP_EOL;
        echo '    FAILED:  ' . count ($testResults ['failed']) . ' tests' . PHP_EOL . PHP_EOL;
        
        if (count ($testResults ['failed']) > 0)
        {
                $failedPerFile = array ();
                foreach ($testResults ['failed'] as $testInfo)
                {
                        $classInfo = new \ ReflectionClass ($testInfo ['test']);
                        $fileName  = $classInfo -> getFileName ();
                        
                        if (!isset ($failedPerFile [$fileName]))
                        {
                                $failedPerFile [$fileName] = array ();
                        }
                        
                        $failedPerFile [$fileName] [] = array
                        (
                                'name'          => str_replace ('\\', '::', $classInfo -> name) . '::' . $testInfo ['test'] -> getName (),
                                'reason'        => $testInfo ['reason']
                        );
                }
                
                foreach ($failedPerFile as $fileName => $testsFailed)
                {
                        echo 'File "' . str_replace (__DIR__ . '/', '', $fileName) . '":' . PHP_EOL;
                        foreach ($testsFailed as $test)
                        {
                                echo '  Test "' . $test ['name'] . '" failed: ' . $test ['reason'] . PHP_EOL;
                        }
                        
                        echo PHP_EOL;
                }
        }
        else
        {
                echo 'All tests passed.' . PHP_EOL . PHP_EOL;
        }
        
        die ();
}
